#!/usr/local/bin/python
# Rob Dennis - Lurchington Q. Delacroix
# Keasby - TV renamer
# Project Homepage: http://code.google.com/p/keasby/

#--- Python Imports
import logging
from string import Template

#--- Keasby Imports
from keasby_base import KeasbyError
from keasby_options import kopt

from keasby_toolbox import log

class TvBase(object):
    """
    Base class for ensuring standard attributes across all children
    """
    #without these, an episode isn't accurately specified
    required_keys = ()
    #standard key names
    valid_keys = (("episodename", unicode, "name of episode"),
                    ("episodenumber", int, "episode number of a given season"),
                    ("seasonnumber", int, "season number"),
                    ("seriesname", unicode,
                        "name of the television show/series"))

    #top level keys hold information not meant to be in the "info" tab
    top_level_keys = ("parts")

    def __init__(self):

        log.debug("initializing %s"%self.__class__.__name__)
        self.info = dict()
        self.info["required"] = dict()
        self.info["extra"] = dict()

#--- Setting TvEpisode Attributes

    def __setattr__(self, name, value):
        """
        Sets attributes for the class, including casting specific attributes
        as a number
        """

        if all([hasattr(self, "info"),
                hasattr(self, "required"),
                hasattr(self, "extra")]) and name not in self.top_level_keys:

            log.debug("setting %s to %s post-initialization. Required: %s",
                        name, value, str(name in self.required_keys))

            if name in self.__dict__["info"]:
                self.__dict__["info"][name] = value

##            elif name in self.__dict__["info"]["required"]:
##                self.__dict__["info"]["required"][name] = self.type_cast(name,
##                                                                        value)


            elif name in self.required_keys:
                self.__dict__["info"]["required"][name] = self.type_cast(name,
                                                                        value)
            else:
                self.__dict__["info"]["extra"][name] = self.type_cast(name,
                                                                        value)

        else:
            object.__setattr__(self, name, value)

    @staticmethod
    def type_cast(name, value, multi_possible = None):
        """
        Function for casting specific attributes as specific data types
        """

        if multi_possible is None:
            multi_possible = name in TvEpisode.multi_attr


        try:
            if name in TvEpisode.int_attr:
                log.debug("setting an integer value (%s) for %s", value, name)
                return int(value)

            elif isinstance(value, str):
                log.debug("casting string as unicode")
                return unicode(value)

            else:
                log.debug("not re-casting value (%s) for %s", value, name)
                return value

        except TypeError as detail:
            raise TvEpisodeError(detail)

        except ValueError:
            log.debug("attempting to split episode: %s->%s", name, value)

            splitchar = kopt.cl_opts.split_episode_char

            if multi_possible and splitchar in value:
                raise TvEpisodeHasMultiplesError("for attribute %s"%name+
                                                    " and data: %s,"%value+
                                                    " using %s,"%splitchar)


class TvSeries(TvBase):
    """
    TvSeries class represents a related group of TvEpisodes (or TvSeries),
    for example, a season, a mini-series, a full run of a show.
    """
    required_keys = ("seriesname")

    def __init__(self, *parts, **common_info):

        TvBase.__init__(self)

        log.debug("initializing %s, with %d parts and common info: %s",
                    self.__class__.__name__,
                    len(parts), str(common_info))


        #self.parts = [TvEpisodeFactory.single_new(part) for part in parts]
        self.parts = [TvEpisodeFactory.new(part) for part in parts]

        for name, value in common_info.iteritems():
            setattr(self, name, value)

        log.debug("completed appending subepisodes")

    def to_dict(self):

        if hasattr(self, "parts"):
            log.debug("returning dictionaries for subparts and common")
            return [part.to_dict() for part in self.parts] + [self.info]

        raise TvSeriesError("Unable to return dictionary representation"+
                                    " for %s"%self.__class__.__name__)


    def __eq__(self, comparator):
        """
        Defines equality for a TvSeries as the equality of all subparts\
        and common information to a comparator
        """

        log.debug("comparing equality for %s"%self.__class__.__name__)

        if isinstance(comparator, TvSeries):
            return all((self_part==comparator_part
                        for self_part, comparator_part
                            in zip(self.parts, comparator.parts)))

        else:
            return NotImplemented

    def __ne__(self, comparactor):

        log.debug("comparing inequality for %s"%self.__class__.__name__)

        result = self.__eq__(self, comparator)

        if result is not NotImplemented:
            return not result
        else:
            return result




    def __str__(self):
        return self


#--- Getting TvEpisode Attributes

    def __getattr__(self, name):
        """
        Accesses parts of the TvEpisode
        """
        if name in self.__dict__["info"]:
            return self.__dict__["info"][name]

        elif name in self.__dict__["info"]["required"]:
            return self.__dict__["info"]["required"][name]

        elif name in self.__dict__["info"]["extra"]:
            return self.__dict__["info"]["extra"][name]

        else:
            raise AttributeError("%s has no"%self.__class__.__name__+
                                    " attribute %s"%name+
                                    " %s"%str(self.__dict__))



class TvEpisode(TvBase):
    """
    TvEpisode class represents a single episode of television
    """
    required_keys = ("seriesname", "seasonnumber", "episodenumber")
    int_attr = ("episodenumber", "seasonnumber")
    multi_attr = ("episodenumber", "seasonnumber")

    def __init__(self, **kwargs):

        TvBase.__init__(self)

##        self.info["required"] = dict.fromkeys(self.required_keys)
##        self.info["extra"] = dict()

        log.debug("included required keys: %s"%str(self.required_keys))

        for key, value in kwargs.iteritems():
            setattr(self, key, value)

##        self._init_info_from_dict(kwargs)
##
##    def _init_info_from_dict(self, kwargs):
##
##        log.debug("initializing information from dictionary")
##
##        for key, value in kwargs.iteritems():
##            if key not in self.info:
##                self.extra[key] = value
##                setattr(self, key, self.extra[key])
##
##            else:
##                setattr(self, key, value)

#---Methods

    def to_dict(self):
        """
        returns a dictionary representation of the instance
        """

        if hasattr(self, "info"):
            log.debug("returning dictionary: %s"%str(self.info))
            return self.info

        else:
            raise TvEpisodeError("Unable to return dictionary representation"+
                                    " for %s"%self.__class__.__name__)

#--- Getting TvEpisode Attributes

    def __getattr__(self, name):
        """
        Accesses parts of the TvEpisode
        """
        if name in self.__dict__["info"]:
            return self.__dict__["info"][name]

        elif name in self.__dict__["info"]["required"]:
            return self.__dict__["info"]["required"][name]

        elif name in self.__dict__["info"]["extra"]:
            return self.__dict__["info"]["extra"][name]

        else:
            raise AttributeError("%s has no"%self.__class__.__name__+
                                    " attribute %s"%name)

#--- Comparison Operations

    def __eq__(self, comparator):
        """
        Defines equality versus a TvEpisode instance, or an object that can
        be cast as one
        """

        log.debug("comparing equality for %s"%self.__class__.__name__)

        if isinstance(comparator, TvEpisode):
            _self = self.to_dict()["required"]
            _compare = comparator.to_dict()["required"]
            return _self == _compare

        elif isinstance(comparator, dict):
            _self = self.to_dict()["required"]
            _compare = TvEpisode(comparator).to_dict()["required"]
            return _self == _compare

        else:
            return NotImplemented

    def __ne__(self, comparator):

        log.debug("comparing inequality for %s"%self.__class__.__name__)

        result = self.__eq__(self, comparator)

        if result is not NotImplemented:
            return not result
        else:
            return result


    def __str__(self):
        return str(self.__dict__)


class TvMultiEpisode(TvSeries, TvEpisode):
    """
    TvMultiEpisode is a specialized type of TvSeries that's intended for
    holding 1 file that encompasses multiple episodes.
    """
    required_keys = ("seriesname", "seasonnumber", "episodenumber")

    def __init__(self, *sub_eps, **common_kwargs):
        TvSeries.__init__(self, *sub_eps, **common_kwargs)


class TvEpisodeFactory(object):
    """
    Class for abstracting the creation of TvEpisode and TvMultiEpisode
    """
    @staticmethod
    def new(*args, **kwargs):
        """
        returns TvEpisode or TvMultiEpisode instance based on given values
        """

        log.debug("called factory method with\nargs: %s\nkwargs: %s", str(args),
                                                                    str(kwargs))

        if args:
            if len(args)>1:
                log.debug("Factory assumes sub-episodes; "+
                            "returning TvMultiEpisode")
                if "common_info" in kwargs:
                    log.debug("common_info detected: %s/nNot searching",
                                str(kwargs["common_info"]))
                else:
                    log.debug("finding common_info for all parts")

                    common = TvEpisodeFactory._find_common_info(*args)

                    if common:
                        log.debug("found common information: %s", str(common))
                        kwargs.update(common)

                        log.debug("removing common parts from sub episodes")
                        for common_pair in common:
                            for arg in args:
                                del arg[common_pair[0]]

                    else:
                        log.debug("found no common information")


                return TvMultiEpisode(*args, **kwargs)
            else:
                return TvEpisodeFactory._single_new(args[0])
        else:
            if not kwargs:
                log.warning("Called TvEpisodeFactory with empty arguments")

        _split_char = kopt.cl_opts.split_episode_char

        split_keys = [multi_key for multi_key in TvEpisode.multi_attr
                        if multi_key in kwargs and
                            _split_char in str(kwargs[multi_key])]

        if not split_keys:
            log.debug("Factory assumes single episode; returning TvEpisode")
            return TvEpisode(**kwargs)
        else:
            log.debug("Factory assumes multi episode; returning TvMultiEpisode")
            return TvEpisodeFactory._make_TvMultiEpisode(kwargs, split_keys)

    @staticmethod
    def _single_new(arg):
        """
        Intended to handle receiving a dictionary argument and cast \
        as necessary
        """

        if isinstance(arg, dict):
            return TvEpisodeFactory.new(**arg)
        elif isinstance(arg, TvBase):
            return arg
        else:
            raise TvEpisodeFactoryError("Unable to return a TvEpisode "+
                                        "instance using argument %s of %s"%
                                        (str(arg), type(arg)))

    @staticmethod
    def _find_common_info(*maps):
        """
        return an array of key-value pairs that are common to all parts
        """

        if not maps:
            raise TvEpisodeFactoryError("must specify valid maps to compare")
        if maps and len(maps)==1:
            return maps[0].items()

        mysets = (set(map.items()) for map in maps)

        return reduce(set.intersection, mysets)


    @staticmethod
    def _make_TvMultiEpisode(info_map, split_keys,
                             split_char=kopt.cl_opts.split_episode_char):
        """
        Return an array of TvMultiEpisode instances, split based on the presence
        of a multi-episode split character
        """
        import re

        log.debug("testing sub-ep split from %s"%str(info_map))

        #popping from the dictionary removes the unique keys
        map_list = [[(key, split) for split in
                        re.split(r"%s"%split_char, info_map.pop(key))]
                    for key in split_keys]

        unique_maps = [TvEpisode(**dict(split_map))
                            for split_map in zip(*map_list)]

        log.debug("attempting to split episode into subepisodes")

        return TvMultiEpisode(*unique_maps, **info_map)

#--- Exceptions

class TvBaseError(KeasbyError):
    """
    TvBase Exception
    """
    pass

class TvSeriesError(TvBaseError):
    """
    TvSeries Exception
    """
    pass

class TvEpisodeError(TvBaseError):
    """
    TvEpisode Exception
    """
    pass

class TvMultiEpisodeError(TvSeriesError, TvEpisodeError):
    """
    TvSeries Exception
    """
    pass

class TvEpisodeHasMultiplesError(TvEpisodeError):
    """
    thrown to signal that the provided information implies a file
    with multiple episodes
    """
    pass

class TvEpisodeFactoryError(TvBaseError):
    """
    TvEpisodeFactory Exception
    """

if __name__ == "__main__":

    pass
